package com.filldream.fastboot.core.service.websocket;

import com.filldream.fastboot.base.config.MyEndpointConfigure;
import com.filldream.fastboot.base.entity.BaseParam;
import com.filldream.fastboot.base.request.common.baseParam.BaseParamListSTO;
import com.filldream.fastboot.base.response.pc.SystemLoginDTO;
import com.filldream.fastboot.common.util.StringUtil;
import com.filldream.fastboot.core.cache.CacheUtil;
import com.filldream.fastboot.core.manager.BaseParamManager;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.input.ReversedLinesFileReader;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author RickSun
 * @Description WebSocket获取实时日志并输出到Web页面
 * 优化日期：2023-12-07
 * 代码贡献者 RickSun、张曦
 * 老 版 本：参考gitee 2023-12-07之前的代码
 * 注意每次仅能读取closeSize行日志，即默认20000行
 * 可使用http://www.websocket-test.com/在线调试该功能
 **/
@Slf4j
@Component
@ServerEndpoint(value = "/websocket/logging/{token}", configurator = MyEndpointConfigure.class)
public class WsLogServer {
    // 匹配日期开头加换行，2019-08-12 14:15:04
    // ^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}
    public static Pattern dateTimePattern = Pattern.compile("\\d{4}-\\d{2}-\\d{2}&nbsp;\\d{2}:\\d{2}:\\d{2}\\.\\d{3}");
    public static Pattern dateTime2Pattern = Pattern.compile("\\d{2}-\\d{2}&nbsp;\\d{2}:\\d{2}:\\d{2}\\.\\d{3}");
    /***
     * 请求id正则
     */
    public static Pattern requestIdPattern = Pattern.compile("</span>\\[\\d{20}]");

    /**
     * 日期颜色
     */
    public static final String DATE_TIME_COLOR = "#fb975e";
    /**
     * debug颜色
     */
    public static final String DEBUG_COLOR = "#7c7e83";
    /**
     * info颜色
     */
    public static final String INFO_COLOR = "#a5db75";
    /**
     * warn颜色
     */
    public static final String WARN_COLOR = "#ffd866";
    /**
     * 错误颜色
     */
    public static final String ERROR_COLOR = "#ff6188";

    /**
     * 类名颜色
     */
    public static final String CLASS_NAME_COLOR = "#78dce8";

    /**
     * 请求id颜色
     */
    public static final String REQUEST_ID_COLOR = "#aa9cef";

    @Value("${project.log.filePath}")
    private String logFilePath;

    @Value("${project.safe.test}")
    private boolean test;

    {
        boolean windows = System.getProperty("os.name").toLowerCase().contains("windows");
        if (windows) {
            logFilePath = "D:\\root\\web\\service\\log\\fastboot.log";
        }
    }

    /**
     * 连接集合
     */
    private static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
    private static final Map<String, Integer> lengthMap = new ConcurrentHashMap<>();
    private static final Map<String, String> tokenIdMap = new ConcurrentHashMap<>();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(@PathParam("token") String token, Session session) {


        Session nowSession;
        if (tokenIdMap.containsKey(token)) {
            nowSession = sessionMap.get(tokenIdMap.get(token));
            log.info("LoggingWebSocketServer-已存在连接，将其关闭：" + token);
            send(nowSession, "<span style=\"color: " + ERROR_COLOR + ";font-weight:bold;\">已有其他程序连接此WebSocket，您的连接已关闭...</span><br/>");
            onClose(nowSession);
        }
        nowSession = session;
        tokenIdMap.put(token, nowSession.getId());

        boolean checkToken = checkToken(token, nowSession);

        if (checkToken) {
            // 添加到集合中
            sessionMap.put(nowSession.getId(), nowSession);
            lengthMap.put(nowSession.getId(), 1);// 默认从第一行开始
            tokenIdMap.put(token, nowSession.getId());

            sendLogMsg(nowSession);
        }

    }

    public void sendLogMsg(Session session) {
        // 获取日志信息
        new Thread(() -> {
            log.info("LoggingWebSocketServer-FastBoot任务开始,每次最多读取20000行日志,连接数:" + (ObjectUtils.isEmpty(sessionMap) ? 0 : sessionMap.size()));
            int maxSize = 100; //一次读取的行数
            int closeSize = 20000; //总共读取的行数，超过closeSize行自动断开ws
            int nowSize = 0;
            String line = "";
            ReversedLinesFileReader fileReader = null;
            LinkedHashSet<String> logSet = null;
            LinkedHashSet<String> sendSet = new LinkedHashSet<>();

            while (sessionMap.get(session.getId()) != null) {
                logSet = new LinkedHashSet<>();
                nowSize = 0;
                line = "";
                try {
                    fileReader = new ReversedLinesFileReader(new File(logFilePath));
                    while ((line = fileReader.readLine()) != null && nowSize < maxSize) {
                        nowSize += 1;
                        logSet.add( line );
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }finally {
                    if(fileReader != null){
                        try {
                            fileReader.close();
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }

                List<String> objects = new ArrayList<>(logSet);
                Collections.reverse(objects);
                logSet = new LinkedHashSet<>();
                for (String item : objects) {
                    logSet.add(item);
                }

                for (String item : logSet) {
                    if( !sendSet.contains(item) ){
                        try {
                            send(session, processLogColor(item));
                        }catch (Exception e2){
                            System.out.println("错误----------->" + item);
                        }
                        sendSet.add( item );
                    }

                    int sendSize = sendSet.size();
                    if( sendSize >= closeSize ){
                        sendSet.clear();
                        logSet.clear();
                        // 从集合中删除
                        AtomicReference<String> token = new AtomicReference<>("");
                        tokenIdMap.forEach((k, v) -> {
                            if (v.equals(session.getId())) {
                                token.set(k);
                            }
                        });
                        tokenIdMap.remove(token.get());
                        sessionMap.remove(session.getId());
                        lengthMap.remove(session.getId());
                    }
                }

            }
            log.info("LoggingWebSocketServer-FastBoot任务结束,连接数:" + sessionMap.size());
        }).start();
    }

    private String processLogColor(String line) {
        // 去掉颜色字符乱码
        line = removeMessyCode(line);

        // 先转义
        line = htmlTransferredMeaning(line);

        // 处理等级
        boolean hasLogLevel = line.contains("DEBUG") || line.contains("INFO") || line.contains("WARN") || line.contains("ERROR");
        if (hasLogLevel) {
            line = processDateTimeColor(line);
            // 处理日志等级颜色
            line = processLogLevelColor(line);
            // 处理类名颜色
            line = processClassNameColor(line);
            // 处理requestId颜色
            line = processRequestIdColor(line);
        }
        line = checkErrorMsg(line);
        line = "<div class=\"websocket-log-line\">" + line + "</div>";
        return line;
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session) {
        // 从集合中删除
        AtomicReference<String> token = new AtomicReference<>("");
        tokenIdMap.forEach((k, v) -> {
            if (v.equals(session.getId())) {
                token.set(k);
            }
        });
        tokenIdMap.remove(token.get());
        sessionMap.remove(session.getId());
        lengthMap.remove(session.getId());
    }

    /**
     * 发生错误时调用
     */
    @OnError
    public void onError(Session session, Throwable error) {
        // 输出到日志文件中
        log.error(errorInfoToString(error));
    }

    /**
     * 服务器接收到客户端消息时调用的方法
     */
    @OnMessage
    public void onMessage(String message, Session ignoredSession) {
        log.info(message);
    }

    /**
     * 封装一个send方法，发送消息到前端
     */
    private void send(Session session, String message) {
        try {
            session.getBasicRemote().sendText(message);
        } catch (Exception e) {
            // 输出到日志文件中
            log.error(errorInfoToString(e));
        }
    }

    public boolean checkToken(String pcToken, Session session) {
        // 查看是否开启websocket日志开关
        BaseParam openParam = BaseParamManager.getOneByCode(new BaseParamListSTO().setParamType(1).setParamCode("is_open_wslog"));
        if (openParam == null || openParam.getParamValue().equals("0")) {
            send(session, "<span style=\"color: " + ERROR_COLOR + ";\">***********已关闭WS日志监控功能，若开启请前往'字典管理-参数管理'中配置is_open_wslog参数为'1'***********</span>");
            onClose(session);
            return false;
        }


        boolean isOk = false;
        if (!test) {
            SystemLoginDTO loginDTO = CacheUtil.checkPcToken(pcToken);
            if ( loginDTO != null) {
                if (loginDTO.getRoleId().equals(1L)) {
                    isOk = true;
                }
            }
        } else {
            isOk = true;
        }
        if (!isOk) {
            send(session, "<span style=\"color: " + ERROR_COLOR + ";\">***********您无权查看日志!已关闭连接！原因如下:1、地址错误 2、测试开关为false 3、日志文件路径问题 4、登录中止 5、仅超级管理员角色可查看日志 6、网络问题***********</span>");
            onClose(session);
        }
        return isOk;
    }

    /**
     * Exception出错的栈信息转成字符串
     * 用于打印到日志中
     */
    public static String errorInfoToString(Throwable e) {
        // try-with-resource语法糖 处理机制
        try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
            e.printStackTrace(pw);
            pw.flush();
            sw.flush();
            return sw.toString();
        } catch (Exception e2) {
            throw new RuntimeException(e2.getMessage(), e2);
        }
    }

    public String checkErrorMsg(String msg) {
        if (org.apache.commons.lang3.StringUtils.isNotBlank(msg)) {
            if (msg.contains("Exception")) {
                return "<span style=\"color: " + ERROR_COLOR + ";\">" + msg + "</span>";
            }
        }
        return msg;
    }

    /**
     * 去除乱码
     *
     * @param line 当前行
     * @return 去除乱码后的行
     */
    private String removeMessyCode(String line) {
        // 去掉颜色字符乱码，因为logback打日志的时候带上了颜色
        line = line.replaceAll("\u001B\\[2m", "")
                .replaceAll("\u001B\\[31m", "")
                .replaceAll("\u001B\\[32m", "")
                .replaceAll("\u001B\\[33m", "")
                .replaceAll("\u001B\\[34m", "")
                .replaceAll("\u001B\\[35m", "")
                .replaceAll("\u001B\\[36m", "")
                .replaceAll("\u001B\\[37m", "")
                .replaceAll("\u001B\\[38m", "")
                .replaceAll("\u001B\\[39m", "")
                .replaceAll("\u001B\\[1;32m", "")
                .replaceAll("\u001B\\[1;33m", "")
                .replaceAll("\u001B\\[1;34m", "")
                .replaceAll("\u001B\\[1;35m", "")
                .replaceAll("\u001B\\[0;35m", "")
                .replaceAll("\u001B\\[0;39m", "")
                .replaceAll("\u001B\\[1;39m", "")
                .replaceAll("\u001B\\[1;31m", "")
                .replaceAll("\u001B\\[0;31m", "")
                .replaceAll("\u001B\\[1;33m", "")
                .replaceAll("\u001B\\[0;33m", "")
                .replaceAll("\u001B\\[1;34m", "")
                .replaceAll("\u001B\\[0;34m", "")
                .replaceAll("\u001B\\[33m", "")
        ;
        return line;
    }

    /**
     * HTML转义
     *
     * @param line 当前行
     * @return 转义后的行
     */
    private String htmlTransferredMeaning(String line) {
        line = line.replaceAll("&", "&amp;")
                .replaceAll("<", "&lt;")
                .replaceAll(">", "&gt;")
                .replaceAll("\t", "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;")
                .replaceAll(" ", "&nbsp;")
                .replaceAll("\"", "&quot;");
        return line;
    }

    /**
     * 处理日志中的日期时间
     *
     * @param line 当前行
     * @return 日期加颜色行
     */
    private String processDateTimeColor(String line) {
        Matcher matcher = dateTimePattern.matcher(line);
        if (matcher.find()) {
            String dateTime = matcher.group();
            line = line.replace(dateTime, "<span style=\"color: " + DATE_TIME_COLOR + "\">" + dateTime + "</span>");
        } else {
            matcher = dateTime2Pattern.matcher(line);
            if (matcher.find()) {
                String dateTime = matcher.group();
                line = line.replace(dateTime, "<span style=\"color: " + DATE_TIME_COLOR + "\">" + dateTime + "</span>");
            }
        }
        return line;
    }


    /**
     * 处理日志等级颜色
     *
     * @param line 当前行
     * @return 处理后的日志行
     */
    private String processLogLevelColor(String line) {
        if (line.contains("DEBUG")) {
            line = line.replace("DEBUG", "<span style=\"color: " + DEBUG_COLOR + ";\">DEBUG</span> ");
            line = processTextColor(line, DEBUG_COLOR);
        } else if (line.contains("INFO")) {
            line = line.replace("INFO", "<span style=\"color: " + INFO_COLOR + ";\">INFO</span> ");
            line = processTextColor(line, INFO_COLOR);
        } else if (line.contains("WARN")) {
            line = line.replace("WARN", "<span style=\"color: " + WARN_COLOR + ";\"><b>WARN</b></span> ");
            line = processTextColor(line, WARN_COLOR);
        } else if (line.contains("ERROR")) {
            line = line.replace("ERROR", "<span style=\"color:  " + ERROR_COLOR + ";\"><b>ERROR</b></span> ");
            line = processTextColor(line, ERROR_COLOR);
        }
        return line;
    }

    private String processTextColor(String line, String color) {
        if (StringUtils.isBlank(line)) {
            return line;
        }
        String[] split = line.split("]&nbsp;:&nbsp;");
        if (split.length == 2) {
            line = split[0] + "]&nbsp;:&nbsp;<span style=\"color: " + color + "\">" + split[1] + "</span>";
        }
        return line;
    }

    /**
     * 处理类名颜色
     *
     * @param line 当前行
     * @return 处理后的行
     */
    private String processClassNameColor(String line) {
        // 处理类名
        int classStart = line.indexOf("]");
        // 裁剪
        int classEnd = StringUtil.findStr(line, "\\[", 2);
        if (classEnd < 0) {
            classEnd = StringUtil.findStr(line, "-", 3);
        }
        if (classStart >= 0 && classEnd >= 0) {
            String classLeftStr = line.substring(0, classStart + 1);
            String classStr = "";
            try {
                classStr = line.substring(classStart + 1, classEnd);
            } catch (Exception e) {
                System.out.println("---------------------------------");
                System.out.println(classStart + "---" + classEnd);
                System.out.println(line);
                System.out.println("---------------------------------");
            }
            String classRightStr = line.substring(classEnd);
            classStr = "<span style=\"color:" + CLASS_NAME_COLOR + "\">" + classStr + "</span>";
            line = classLeftStr + classStr + classRightStr;
        }
        return line;
    }

    /**
     * 处理请求id颜色
     *
     * @param line 当前行
     * @return 处理后的行
     */
    private String processRequestIdColor(String line) {
        // 处理requestId
        Matcher requestIDMatch = requestIdPattern.matcher(line);
        if (requestIDMatch.find()) {
            String requestId = requestIDMatch.group();
            requestId = requestId.replace("</span>", "");
            line = line.replace(requestId, "<span style=\"color: " + REQUEST_ID_COLOR + "\">" + requestId + "</span>");
        }
        return line;
    }
}
